/*!
 * jQuery Mobile Path Utility @VERSION
 * http://jquerymobile.com
 *
 * Copyright jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 */

//>>label: Path Helpers
//>>group: Navigation
//>>description: Path parsing and manipulation helpers
//>>docs: http://api.jquerymobile.com/category/methods/path/

( function( factory ) {
	if ( typeof define === "function" && define.amd ) {

		// AMD. Register as an anonymous module.
		define( [
			"jquery",
			"./../ns" ], factory );
	} else {

		// Browser globals
		factory( jQuery );
	}
} )( function( $ ) {

var path, $base,
	dialogHashKey = "&ui-state=dialog";

$.mobile.path = path = {
	uiStateKey: "&ui-state",

	// This scary looking regular expression parses an absolute URL or its relative
	// variants (protocol, site, document, query, and hash), into the various
	// components (protocol, host, path, query, fragment, etc that make up the
	// URL as well as some other commonly used sub-parts. When used with RegExp.exec()
	// or String.match, it parses the URL into a results array that looks like this:
	//
	//     [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content
	//     [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
	//     [2]: http://jblas:password@mycompany.com:8080/mail/inbox
	//     [3]: http://jblas:password@mycompany.com:8080
	//     [4]: http:
	//     [5]: //
	//     [6]: jblas:password@mycompany.com:8080
	//     [7]: jblas:password
	//     [8]: jblas
	//     [9]: password
	//    [10]: mycompany.com:8080
	//    [11]: mycompany.com
	//    [12]: 8080
	//    [13]: /mail/inbox
	//    [14]: /mail/
	//    [15]: inbox
	//    [16]: ?msg=1234&type=unread
	//    [17]: #msg-content
	//
	urlParseRE: /^\s*(((([^:\/\\#\?]+:)?(?:([\/\\]{2})((?:(([^:@\/\\#\?]*)(?:\:([^@\/\\#\?]*))?)@)?(([^:\/\\#\?\]\[]+|\[[^\/\\\]@#?]+\])(?:\:([0-9]+))?))?)?)?(([\/\\]?(?:[^\/\\\?#]+[\/\\]+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,

	// Abstraction to address xss (Issue #4787) by removing the authority in
	// browsers that auto-decode it. All references to location.href should be
	// replaced with a call to this method so that it can be dealt with properly here
	getLocation: function( url ) {
		var parsedUrl = this.parseUrl( url || location.href ),
			uri = url ? parsedUrl : location,

			// Make sure to parse the url or the location object for the hash because using
			// location.hash is autodecoded in firefox, the rest of the url should be from
			// the object (location unless we're testing) to avoid the inclusion of the
			// authority
			hash = parsedUrl.hash;

		// mimic the browser with an empty string when the hash is empty
		hash = hash === "#" ? "" : hash;

		return uri.protocol +
			parsedUrl.doubleSlash +
			uri.host +

			// The pathname must start with a slash if there's a protocol, because you
			// can't have a protocol followed by a relative path. Also, it's impossible to
			// calculate absolute URLs from relative ones if the absolute one doesn't have
			// a leading "/".
			( ( uri.protocol !== "" && uri.pathname.substring( 0, 1 ) !== "/" ) ?
				"/" : "" ) +
			uri.pathname +
			uri.search +
			hash;
	},

	//return the original document url
	getDocumentUrl: function( asParsedObject ) {
		return asParsedObject ? $.extend( {}, path.documentUrl ) : path.documentUrl.href;
	},

	parseLocation: function() {
		return this.parseUrl( this.getLocation() );
	},

	//Parse a URL into a structure that allows easy access to
	//all of the URL components by name.
	parseUrl: function( url ) {
		// If we're passed an object, we'll assume that it is
		// a parsed url object and just return it back to the caller.
		if ( $.type( url ) === "object" ) {
			return url;
		}

		var matches = path.urlParseRE.exec( url || "" ) || [];

		// Create an object that allows the caller to access the sub-matches
		// by name. Note that IE returns an empty string instead of undefined,
		// like all other browsers do, so we normalize everything so its consistent
		// no matter what browser we're running on.
		return {
			href: matches[ 0 ] || "",
			hrefNoHash: matches[ 1 ] || "",
			hrefNoSearch: matches[ 2 ] || "",
			domain: matches[ 3 ] || "",
			protocol: matches[ 4 ] || "",
			doubleSlash: matches[ 5 ] || "",
			authority: matches[ 6 ] || "",
			username: matches[ 8 ] || "",
			password: matches[ 9 ] || "",
			host: matches[ 10 ] || "",
			hostname: matches[ 11 ] || "",
			port: matches[ 12 ] || "",
			pathname: matches[ 13 ] || "",
			directory: matches[ 14 ] || "",
			filename: matches[ 15 ] || "",
			search: matches[ 16 ] || "",
			hash: matches[ 17 ] || ""
		};
	},

	//Turn relPath into an asbolute path. absPath is
	//an optional absolute path which describes what
	//relPath is relative to.
	makePathAbsolute: function( relPath, absPath ) {
		var absStack,
			relStack,
			i, d;

		if ( relPath && relPath.charAt( 0 ) === "/" ) {
			return relPath;
		}

		relPath = relPath || "";
		absPath = absPath ? absPath.replace( /^\/|(\/[^\/]*|[^\/]+)$/g, "" ) : "";

		absStack = absPath ? absPath.split( "/" ) : [];
		relStack = relPath.split( "/" );

		for ( i = 0; i < relStack.length; i++ ) {
			d = relStack[ i ];
			switch ( d ) {
			case ".":
				break;
			case "..":
				if ( absStack.length ) {
					absStack.pop();
				}
				break;
			default:
				absStack.push( d );
				break;
			}
		}
		return "/" + absStack.join( "/" );
	},

	//Returns true if both urls have the same domain.
	isSameDomain: function( absUrl1, absUrl2 ) {
		return path.parseUrl( absUrl1 ).domain.toLowerCase() ===
			path.parseUrl( absUrl2 ).domain.toLowerCase();
	},

	//Returns true for any relative variant.
	isRelativeUrl: function( url ) {
		// All relative Url variants have one thing in common, no protocol.
		return path.parseUrl( url ).protocol === "";
	},

	//Returns true for an absolute url.
	isAbsoluteUrl: function( url ) {
		return path.parseUrl( url ).protocol !== "";
	},

	//Turn the specified realtive URL into an absolute one. This function
	//can handle all relative variants (protocol, site, document, query, fragment).
	makeUrlAbsolute: function( relUrl, absUrl ) {
		if ( !path.isRelativeUrl( relUrl ) ) {
			return relUrl;
		}

		if ( absUrl === undefined ) {
			absUrl = this.documentBase;
		}

		var relObj = path.parseUrl( relUrl ),
			absObj = path.parseUrl( absUrl ),
			protocol = relObj.protocol || absObj.protocol,
			doubleSlash = relObj.protocol ? relObj.doubleSlash : ( relObj.doubleSlash || absObj.doubleSlash ),
			authority = relObj.authority || absObj.authority,
			hasPath = relObj.pathname !== "",
			pathname = path.makePathAbsolute( relObj.pathname || absObj.filename, absObj.pathname ),
			search = relObj.search || ( !hasPath && absObj.search ) || "",
			hash = relObj.hash;

		return protocol + doubleSlash + authority + pathname + search + hash;
	},

	//Add search (aka query) params to the specified url.
	addSearchParams: function( url, params ) {
		var u = path.parseUrl( url ),
			p = ( typeof params === "object" ) ? $.param( params ) : params,
			s = u.search || "?";
		return u.hrefNoSearch + s + ( s.charAt( s.length - 1 ) !== "?" ? "&" : "" ) + p + ( u.hash || "" );
	},

	convertUrlToDataUrl: function( absUrl ) {
		var result = absUrl,
			u = path.parseUrl( absUrl );

		if ( path.isEmbeddedPage( u ) ) {
			// For embedded pages, remove the dialog hash key as in getFilePath(),
			// and remove otherwise the Data Url won't match the id of the embedded Page.
			result = u.hash
				.split( dialogHashKey )[ 0 ]
				.replace( /^#/, "" )
				.replace( /\?.*$/, "" );
		} else if ( path.isSameDomain( u, this.documentBase ) ) {
			result = u.hrefNoHash.replace( this.documentBase.domain, "" ).split( dialogHashKey )[ 0 ];
		}

		return window.decodeURIComponent( result );
	},

	//get path from current hash, or from a file path
	get: function( newPath ) {
		if ( newPath === undefined ) {
			newPath = path.parseLocation().hash;
		}
		return path.stripHash( newPath ).replace( /[^\/]*\.[^\/*]+$/, "" );
	},

	//set location hash to path
	set: function( path ) {
		location.hash = path;
	},

	//test if a given url (string) is a path
	//NOTE might be exceptionally naive
	isPath: function( url ) {
		return ( /\// ).test( url );
	},

	//return a url path with the window's location protocol/hostname/pathname removed
	clean: function( url ) {
		return url.replace( this.documentBase.domain, "" );
	},

	//just return the url without an initial #
	stripHash: function( url ) {
		return url.replace( /^#/, "" );
	},

	stripQueryParams: function( url ) {
		return url.replace( /\?.*$/, "" );
	},

	//remove the preceding hash, any query params, and dialog notations
	cleanHash: function( hash ) {
		return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) );
	},

	isHashValid: function( hash ) {
		return ( /^#[^#]+$/ ).test( hash );
	},

	//check whether a url is referencing the same domain, or an external domain or different protocol
	//could be mailto, etc
	isExternal: function( url ) {
		var u = path.parseUrl( url );

		return !!( u.protocol &&
			( u.domain.toLowerCase() !== this.documentUrl.domain.toLowerCase() ) );
	},

	hasProtocol: function( url ) {
		return ( /^(:?\w+:)/ ).test( url );
	},

	isEmbeddedPage: function( url ) {
		var u = path.parseUrl( url );

		//if the path is absolute, then we need to compare the url against
		//both the this.documentUrl and the documentBase. The main reason for this
		//is that links embedded within external documents will refer to the
		//application document, whereas links embedded within the application
		//document will be resolved against the document base.
		if ( u.protocol !== "" ) {
			return ( !this.isPath( u.hash ) && u.hash && ( u.hrefNoHash === this.documentUrl.hrefNoHash || ( this.documentBaseDiffers && u.hrefNoHash === this.documentBase.hrefNoHash ) ) );
		}
		return ( /^#/ ).test( u.href );
	},

	squash: function( url, resolutionUrl ) {
		var href, cleanedUrl, search, stateIndex, docUrl,
			isPath = this.isPath( url ),
			uri = this.parseUrl( url ),
			preservedHash = uri.hash,
			uiState = "";

		// produce a url against which we can resolve the provided path
		if ( !resolutionUrl ) {
			if ( isPath ) {
				resolutionUrl = path.getLocation();
			} else {
				docUrl = path.getDocumentUrl( true );
				if ( path.isPath( docUrl.hash ) ) {
					resolutionUrl = path.squash( docUrl.href );
				} else {
					resolutionUrl = docUrl.href;
				}
			}
		}

		// If the url is anything but a simple string, remove any preceding hash
		// eg #foo/bar -> foo/bar
		//    #foo -> #foo
		cleanedUrl = isPath ? path.stripHash( url ) : url;

		// If the url is a full url with a hash check if the parsed hash is a path
		// if it is, strip the #, and use it otherwise continue without change
		cleanedUrl = path.isPath( uri.hash ) ? path.stripHash( uri.hash ) : cleanedUrl;

		// Split the UI State keys off the href
		stateIndex = cleanedUrl.indexOf( this.uiStateKey );

		// store the ui state keys for use
		if ( stateIndex > -1 ) {
			uiState = cleanedUrl.slice( stateIndex );
			cleanedUrl = cleanedUrl.slice( 0, stateIndex );
		}

		// make the cleanedUrl absolute relative to the resolution url
		href = path.makeUrlAbsolute( cleanedUrl, resolutionUrl );

		// grab the search from the resolved url since parsing from
		// the passed url may not yield the correct result
		search = this.parseUrl( href ).search;

		// TODO all this crap is terrible, clean it up
		if ( isPath ) {
			// reject the hash if it's a path or it's just a dialog key
			if ( path.isPath( preservedHash ) || preservedHash.replace( "#", "" ).indexOf( this.uiStateKey ) === 0 ) {
				preservedHash = "";
			}

			// Append the UI State keys where it exists and it's been removed
			// from the url
			if ( uiState && preservedHash.indexOf( this.uiStateKey ) === -1 ) {
				preservedHash += uiState;
			}

			// make sure that pound is on the front of the hash
			if ( preservedHash.indexOf( "#" ) === -1 && preservedHash !== "" ) {
				preservedHash = "#" + preservedHash;
			}

			// reconstruct each of the pieces with the new search string and hash
			href = path.parseUrl( href );
			href = href.protocol + href.doubleSlash + href.host + href.pathname + search +
				preservedHash;
		} else {
			href += href.indexOf( "#" ) > -1 ? uiState : "#" + uiState;
		}

		return href;
	},

	isPreservableHash: function( hash ) {
		return hash.replace( "#", "" ).indexOf( this.uiStateKey ) === 0;
	},

	// Escape weird characters in the hash if it is to be used as a selector
	hashToSelector: function( hash ) {
		var hasHash = ( hash.substring( 0, 1 ) === "#" );
		if ( hasHash ) {
			hash = hash.substring( 1 );
		}
		return ( hasHash ? "#" : "" ) + hash.replace( /([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, "\\$1" );
	},

	// return the substring of a filepath before the dialogHashKey, for making a server
	// request
	getFilePath: function( path ) {
		return path && path.split( dialogHashKey )[ 0 ];
	},

	// check if the specified url refers to the first page in the main
	// application document.
	isFirstPageUrl: function( url ) {
		// We only deal with absolute paths.
		var u = path.parseUrl( path.makeUrlAbsolute( url, this.documentBase ) ),

			// Does the url have the same path as the document?
			samePath = u.hrefNoHash === this.documentUrl.hrefNoHash ||
				( this.documentBaseDiffers &&
				u.hrefNoHash === this.documentBase.hrefNoHash ),

			// Get the first page element.
			fp = $.mobile.firstPage,

			// Get the id of the first page element if it has one.
			fpId = fp && fp[ 0 ] ? fp[ 0 ].id : undefined;

		// The url refers to the first page if the path matches the document and
		// it either has no hash value, or the hash is exactly equal to the id
		// of the first page element.
		return samePath &&
			( !u.hash ||
			u.hash === "#" ||
			( fpId && u.hash.replace( /^#/, "" ) === fpId ) );
	},

	// Some embedded browsers, like the web view in Phone Gap, allow
	// cross-domain XHR requests if the document doing the request was loaded
	// via the file:// protocol. This is usually to allow the application to
	// "phone home" and fetch app specific data. We normally let the browser
	// handle external/cross-domain urls, but if the allowCrossDomainPages
	// option is true, we will allow cross-domain http/https requests to go
	// through our page loading logic.
	isPermittedCrossDomainRequest: function( docUrl, reqUrl ) {
		return $.mobile.allowCrossDomainPages &&
			( docUrl.protocol === "file:" || docUrl.protocol === "content:" ) &&
			reqUrl.search( /^https?:/ ) !== -1;
	}
};

path.documentUrl = path.parseLocation();

$base = $( "head" ).find( "base" );

path.documentBase = $base.length ?
	path.parseUrl( path.makeUrlAbsolute( $base.attr( "href" ), path.documentUrl.href ) ) :
	path.documentUrl;

path.documentBaseDiffers = ( path.documentUrl.hrefNoHash !== path.documentBase.hrefNoHash );

//return the original document base url
path.getDocumentBase = function( asParsedObject ) {
	return asParsedObject ? $.extend( {}, path.documentBase ) : path.documentBase.href;
};

return path;
} );
